揭秘!MLPerf Training v2.0飞桨何以力压NGC PyTorch,实现同等GPU配置BERT模型训练性能第一
在6月30日最新发布的MLPerf Training v2.0里,百度使用飞桨框架( PaddlePaddle )和百度智能云百舸计算平台提交的BERT Large模型GPU训练性能结果,在同等GPU配置下的所有提交结果里排名第一,超越了高度定制优化且长期处于领先位置的NGC PyTorch框架,向全世界展现了飞桨框架的性能优势。
飞桨端到端
全流程优化概述
BERT模型是MLPerf Training v2.0自然语言处理领域的唯一基准模型。飞桨在BERT模型8卡GPU训练上创造了世界最优的训练性能,这来自于飞桨框架基础性能与分布式技术的领先性,以及飞桨与NVIDIA GPU的深度协同优化。对于深度学习模型训练任务,从数据读取到模型计算,从底层算子到上层分布式策略,从多设备负载均衡到全流程调度机制,都会影响最终训练性能。
变长序列输入模型的计算加速
数据读取和模型训练的负载均衡
高性能算子和融合优化技术
高加速比的混合并行训练策略
全流程异步执行调度
变长序列
输入模型的计算加速
图3. 序列数据 padding 的示意图
支持变长Encoder计算
BERT Encoder是BERT的主要组成部分,其中的Multi-Head Attention部分的优化是提升BERT训练性能的关键。Multi-Head Attention的主体结构如图4所示。
图4. Multi-Head Attention的主体结构
在参考NVIDIA Apex Library实现的基础上[5],飞桨实现了支持变长的融合 Multi-Head Attention算子,即unpad FMHA(Fused Multi-Head Attention)算子,我们将Encoder模块使用unpad计算方法进行实现。相比padding的方法,unpad的实现传入的输入有一些变化。
首先,如图5所示,输入张量的维度由原来的[batch_size, max_seq_len]被压缩(不同样本的有效 token 被线性平铺)存储为[ntokens],其中batch_size为批大小,max_seq_len为padding的最大序列长度, ntokens代表当前mini-batch有效token总个数。
其次,要求传入能够反映变长序列实际长度的辅助信息,如batch_offset和max_seqlen_cur_bs等,其中batch_offset代表当前mini-batch中样本有效序列长度的前缀和信息,max_seqlen_cur_bs代表当前mini-batch样本的最大序列长度。这些信息会传入到unpad FMHA算子进行辅助计算。
图 5. unpad方法输入张量的存储变化
对BERT Encoder模块使用unpad优化后,端到端获得了大约2.3倍的性能提升。
变长Attention计算优化
if (0 < max_seqlen_cur_bs <= 128)
FMHA_128(...);
else if (128 < max_seqlen_cur_bs <= 256)
FMHA_256(...);
else if (256 < max_seqlen_cur_bs <= 384)
FMHA_384(...);
else if (384 < max_seqlen_cur_bs <= 512)
FMHA_512(...);
由上述选取CUDA Kernel的原则可以发现,根据max_seqlen_cur_bs选取的CUDA Kernel,往往偏向于数值更大的CUDA Kernel。例如,如果一个mini-batch中只有少量序列的长度为512,其他序列的长度都小于128,那么上述原则选取的CUDA Kernel则是FMHA_512,而不是FMHA_128。我们在MLPerf BERT模型实际测试中发现,训练过程中几乎全部选择的都是FMHA_512的 CUDA Kernel,这使得序列长度较小的样本的计算无法达到更好的性能。
为了解决该问题,我们对mini-batch内样本进行分组,让不同小组的样本选择最合适的FMHA Kernel,从而提升整体性能。首先,对mini-batch内样本按照序列长度进行分组,例如根据上述CUDA Kernel选取原则分为 (0, 128], (128, 256], (256, 384], (384, 512] 这4组。然后根据分组情况,每个小组launch相应的 FMHA Kernel。每个小组在调用FMHA Kernel时,需要根据组内的样本个数和token信息重新设置batch_size大小和相关输入的地址偏移。如图6所示,同一个mini-batch的7个样本,根据样本的长度划分成了四个不同的小组(具有相同颜色标识的矩形块属于同一个小组),并分别launch了对应的CUDA Kernel。对于256这个分组(图中黄色的序列),其有2个样本,会调用FMHA_256 Kernel,调用时需设置batch_size为2,max_seqlen_cur_bs为256。
图6. 分组示意图
默认情况下,不同分组的FMHA Kernel在同一个stream中执行。同一个stream中的Kernel会按照Kernel launch的顺序依次来执行。只有当前一个launch的Kernel执行完成,后面launch的Kernel才开始调度。事实上,由于不同分组的FMHA Kernel是独立且没有数据依赖的,可以使用多stream技术进一步提升性能。多stream情况下,调度器会根据当前GPU资源使用情况,同时调度来自不同stream的FMHA Kernel,从而提高GPU的资源利用率,提升Multi-Head Attention模块的整体性能,如图7所示。
图7. FMHA kernels overlap示意图
实验结果显示,对序列数据分组和多stream优化给Multi-Head Attention模块带来了大约20%的性能提升,端到端大约提升了3.5%~3.7%。
支持变长Embedding计算
数据读取
和模型训练的负载均衡
Exchange padding优化
对所有卡上的padding后的输入数据进行AllGather操作,使得每个卡均能获得所有卡上的全量输入数据。 每个卡根据实际序列长度( padding 前的实际序列长度)分别对AllGather后的数据进行排序。 根据每个卡的rank id进行Interleaving Slice操作,得到每个卡上最终的输入数据,即rank id为i的GPU进程获取到的样本编号为i, i + num_devices, i + 2 * num_devices, ...,其中 num_devices为GPU总数。例如,在图9中,Worker 0分到了编号为0和2的样本数据,Worker 1则分到了编号为1和3的样本数据。
图9. Exchange Padding的流程
Exchange Padding操作在CPU端完成,减少GPU端的计算负载。 在模型训练过程中,预先对下一个mini-batch的输入数据进行Exchange Padding操作,使得CPU端的Exchange Padding操作和GPU端的模型训练操作完全overlap起来,消除现有方案每个mini-batch开始前必须串行等待Exchange Padding完成所耗费的时间。
图10. CPU Exchange Padding实现方案的Timeline
Padding移除过程与模型训练的Overlap
高性能算子
和融合优化技术
Embedding算子优化
支持 cuBLASLt GEMM fusions
前向:GEMM + bias融合
反向:GEMM + dbias融合
支持inplace addto优化
dX_1 = ... # X的一部分梯度源自于另一个OP
dX_2 = linear_grad(dOut, Weight)
dX = dX_1 + dX_2
dX_1 = ...
dX.ShareDataWith(dX_1) # dX与dX_1共享显存空间
dX = dX + linear_grad(dOut, Weight) = dX + matmul(dOut, transpose(Weight))
其中,dX=dX+matmul(dOut, transpose(Weight))可以利用cuBlasLt GEMM API设置beta参数等于1来实现,从而将原先的linear_grad和add两个算子融合为一个新的linear_grad算子(cublasLt GEMM API beta=1),减少框架调度开销和Kernel Launch Latency。实际测试结果表明,此优化带来了端到端1.1%的训练加速效果。
算子dropout
与算子residual add+layer_norm融合
高加速比的
混合并行训练策略
在MLPerf BERT模型分布式训练过程中,LAMB优化器需要进行L2-Norm等复杂计算,计算量往往较大。借鉴NVIDIA Apex library的DistributedFusedLAMB的实现,我们在飞桨上实现了优化版本的分布式优化器,其底层原理是通过ZeRO Stage 2[2]的方式,将每个卡上需要更新的参数量减少至原来的1/num_devices (其中,num_devices 为GPU总数),以减少计算开销。
FP16参数梯度的平方和,用于做global norm clipping。 每个参数的L2-Norm。 每个参数对应的Trust Ratio Tensor的L2-Norm。
全流程
异步执行调度
例如,学习率的值是在CPU上计算得到的,却需要在GPU上进行使用( Host to device memory copy )。为了减少CPU、GPU之间的交互,我们将学习率计算挪到GPU上。这可以借助飞桨自定义外部算子的能力,新编写一个计算学习率的算子进行实现。
又如,在每个mini-batch训练完成后,我们常常会打印出当前mini-batch的loss和accuracy等指标,但这会带来额外的Device to host memory copy。在满足MLPerf规则的前提下,我们将原先每个mini-batch均打印一次loss和accuracy的方式改为了每隔335步进行,大幅度地减少了CPU和GPU间的同步开销。
再如,在预测阶段,MLPerf需要统计全量数据集下的Masked Language Model Accuracy指标。常规的实现是,获取每个mini-batch下的Accuracy指标,然后在CPU端进行累加统计,但这会带来每个mini-batch预测均有Device to host memory copy的同步开销。我们通过新增GPU 上的 Accuracy累加统计算子,当且仅当最后一个mini-batch预测时才取出GPU上累计的Accuracy指标的值,减少同步开销。
测试结果显示,上述训练阶段的优化手段在端到端中带来了约0.4%的性能提升,预测阶段的优化手段在端到端中大约提升了0.17% 。
总结
飞桨在MLPerf Training v2.0中获得了BERT模型训练性能世界第一的瞩目成绩。这不仅得益于飞桨框架在性能优化领域的长期耕耘,更离不开硬件生态的助力。近年来,飞桨的技术实力深受广大硬件厂商认可,合作日趋紧密,软硬一体协同发展,生态共创硕果累累。前不久(5月26日),NVIDIA与飞桨合作推出的NGC-Paddle正式上线。同时在本次MLPerf中,Graphcore也通过使用飞桨框架取得了优异成绩。未来,飞桨将继续打造性能优势,在软硬协同性能优化和大规模分布式训练方面持续技术创新,为广大用户提供更加便捷、易用、性能优异的深度学习框架。
本文封面图由AI作画神器文心·一格创作
参考文献
https://github.com/mlcommons/training_results_v2.0
[2]DeepSpeed library.
https://github.com/microsoft/DeepSpeed
[3]DeepLearningExamples library.
https://github.com/NVIDIA/DeepLearningExamples
[4]PaddleNLP library.
https://github.com/PaddlePaddle/PaddleNLP
[5]Apex unpad fmha. https://github.com/NVIDIA/apex
[6]The MLPerf™ Training v1.1 results.
https://github.com/mlcommons/training_results_v1.1
[7]cuBLASLt GEMM fusions.
https://docs.nvidia.com/cuda/cublas/index.html#cublasLtEpilogue_t
关注【飞桨PaddlePaddle】公众号
获取更多技术内容~